/**
 * ************************
 * Project: ePoverty Filename: BarGraph.java
 * Description: A customizable, resizable bar
 * graph Name: Bunna Veth Date: April 6, 2012
 *************************
 */
package epoverty;

import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.Rectangle2D;
import java.text.DecimalFormat;
import javax.swing.JPanel;

public class BarGraph extends JPanel
{
   //Fonts and Colors
   private Font TITLE_FONT = new Font("Arial", Font.BOLD, 16);
   private Font TEXT_FONT = new Font("Arial", Font.PLAIN, 12);
   private Color BACKGROUND_COLOR = Color.BLACK;
   private Color GRADIENT_START = new Color(255, 0, 0);
   private Color GRADIENT_END = new Color(255, 150, 0);
   private Color BAR_COLOR = new Color(255, 255, 255, 200); //transparency allows grid to bleed through
   private int BAR_OPACITY = 180;
   private Color TEXT_COLOR = Color.WHITE;
   private Color GRID_COLOR = Color.WHITE;
   private int GRID_LINES = 5; //includes the x-axis
   //Default Panel Size (used when no size is specified)
   private static final int DEFAULT_WIDTH = 500;
   private static final int DEFAULT_HEIGHT = 300;
   //Margins (spacing around the chart area)
   private final int TOP_MARGIN = 30;
   private final int LEFT_MARGIN = 50;
   private final int BOTTOM_MARGIN = 30;
   private final int RIGHT_MARGIN = 10;
   //Data Fields
   private String title;          //chart title
   private Object[] xLabels;      //labels along the x-axis
   private String[] yLabels;      //labels along the y-axis
   private Object[] dataPoints;   //actual data-points
   private double[] normalValues; //normalized data-points (between 0.0 - 1.0)
   //Dimensional Fields
   private int panelWidth;     //component size
   private int panelHeight;
   private double chartWidth;  //area the bars are drawn in
   private double chartHeight;
   private double cellWidth;   //a cell is the general area where each bar is drawn
   private double barWidth;    //width of each bar (a percentage of the cell width)
   private double gridSpacing; //spacing between horizontal grid-lines

   //Constructor (default size) *******************************************************
   public BarGraph(String title, Object[] xData, Object[] yData)
   {
      this(title, xData, yData, DEFAULT_WIDTH, DEFAULT_HEIGHT);
   }

   //Constructor (specified size)
   //Object[] types provide the flexibility to handle Strings, doubles, ints, or any other numeric types.
   //xData = preferably Strings
   //yData = preferably doubles
   public BarGraph(String title, Object[] xData, Object[] yData, int width, int height)
   {
      this.title = title;
      this.xLabels = xData;
      this.dataPoints = yData;
      setBackground(BACKGROUND_COLOR);

      normalizeData();

      //repaint whenever mouse is moved
      //necessary because this event doesn't normally trigger paintComponent()
      addMouseMotionListener(new MouseMotionAdapter()
      {
         @Override
         public void mouseMoved(MouseEvent event)
         {
            repaint();
         }
      });
   }

   //Compute widths based on the panel size
   private void computeWidths(int width, int height)
   {
      panelWidth = width;
      panelHeight = height;

      chartWidth = width - LEFT_MARGIN - RIGHT_MARGIN;
      chartHeight = height - TOP_MARGIN - BOTTOM_MARGIN;

      cellWidth = chartWidth / dataPoints.length;
      barWidth = 0.6 * cellWidth;
      gridSpacing = chartHeight / (GRID_LINES - 1);
   }

   //Normalize data so it can fit inside the panel
   private void normalizeData()
   {
      //find largest data-point
      double maxValue = Double.parseDouble(dataPoints[0].toString());
      for (Object y : dataPoints)
      {
         double value = Double.parseDouble(y.toString());
         if (value > maxValue)
         {
            maxValue = value;
         }
      }

      //round up to nearest 100 or 1000 (used to get clean divisions)
      if (maxValue < 2000)
      {
         maxValue = 100 * Math.ceil(maxValue / 100);
      }
      else
      {
         maxValue = 1000 * Math.ceil(maxValue / 1000);
      }

      //represent every point as a percentage of the maximum value
      normalValues = new double[dataPoints.length];
      for (int i = 0; i < dataPoints.length; i++)
      {
         normalValues[i] = Double.parseDouble(dataPoints[i].toString()) / maxValue;
      }

      //y-labels (top-to-bottom)
      yLabels = new String[GRID_LINES];
      for (int i = 0; i < GRID_LINES; i++)
      {
         yLabels[i] = String.format("%.0f", (GRID_LINES - i - 1.0) / (GRID_LINES - 1.0) * maxValue);
      }
   }

   //Draw Bar Graph *****************************************************************
   @Override
   public void paintComponent(Graphics g)
   {
      //formating for tooltips
      DecimalFormat df = new DecimalFormat("#.00");

      super.paintComponent(g);
      computeWidths(getWidth(), getHeight()); //dynamically sizes graph if window is resized
      FontMetrics fontMeter; //measures font dimensions

      //title
      g.setColor(TEXT_COLOR);
      g.setFont(TITLE_FONT);
      fontMeter = g.getFontMetrics();
      String titleLabel = title;
      int titleX = (int) (panelWidth / 2.0 - fontMeter.stringWidth(titleLabel) / 2.0); //centers text
      int titleY = (int) fontMeter.getHeight();
      g.drawString(titleLabel, titleX, titleY);

      //gradient background
      Graphics2D g2d = (Graphics2D) g;
      g2d.setPaint(new GradientPaint(0, 0, GRADIENT_START, 0, (int) chartHeight, GRADIENT_END));
      g2d.fill(new Rectangle2D.Double(LEFT_MARGIN, TOP_MARGIN, chartWidth + 1, chartHeight));

      //y-axis
      g.setFont(TEXT_FONT);
      fontMeter = g.getFontMetrics();
      int gridX1 = LEFT_MARGIN;
      int gridX2 = (int) (LEFT_MARGIN + chartWidth);

      for (int i = 0; i < GRID_LINES; i++)
      {
         //grid
         g.setColor(GRID_COLOR);
         int gridY = (int) (i * gridSpacing) + TOP_MARGIN;
         g.drawLine(gridX1, gridY, gridX2, gridY);

         //y-label
         g.setColor(TEXT_COLOR);
         String label = yLabels[i];
         int labelX = LEFT_MARGIN - fontMeter.stringWidth(label) - 5; //right-aligns text
         int labelY = gridY + 5;
         g.drawString(label, labelX, labelY);
      }

      //initialize tooltip variables
      boolean tooltipExists = false;
      String tooltipText = null;
      int mouseX = 0;
      int mouseY = 0;
      Rectangle selectedBar = null; //bar the mouse is hovering over

      //x-axis
      double barOffset = cellWidth / 2.0 - barWidth / 2.0 + LEFT_MARGIN; //centers bar within cell

      for (int i = 0; i < dataPoints.length; i++)
      {
         //bar
         g.setColor(BAR_COLOR);
         int barX = (int) (i * cellWidth + barOffset);
         int barY = (int) (chartHeight - chartHeight * normalValues[i]) + TOP_MARGIN;
         int barHeight = (int) (chartHeight - barY) + TOP_MARGIN;
         g.fillRect(barX, barY, (int) barWidth, barHeight);

         //set tooltip parameters
         Point mousePosition = getMousePosition();
         if (mousePosition != null && !tooltipExists)
         {
            Rectangle bar = new Rectangle(barX, barY, (int) barWidth, barHeight);
            Rectangle mouse = new Rectangle(mousePosition.x - 1, mousePosition.y - 2, 1, 1);
            if (bar.intersects(mouse))
            {
               tooltipExists = true;
               selectedBar = bar;
               tooltipText = "$" +df.format(Double.parseDouble(dataPoints[i].toString())); //text is set to y-value
               mouseX = mousePosition.x;
               mouseY = mousePosition.y;
            }
         }

         //x-label
         g.setColor(TEXT_COLOR);
         String label = xLabels[i].toString();
         if (fontMeter.stringWidth(label) > cellWidth)
         {
            label = label.substring(0, 3); //display the first 3 characters if view is too small
         }
         int labelX = (int) (i * cellWidth + cellWidth / 2.0 - fontMeter.stringWidth(label) / 2.0) + LEFT_MARGIN; //centers text
         int labelY = (int) (TOP_MARGIN + chartHeight) + 15;

         g.drawString(label, labelX, labelY);
      }

      //draw tooltip (draw at the end so it can be above all other elements)
      if (tooltipExists)
      {
         //redraw selected bar to achieve highlight effect
         g.setColor(BAR_COLOR);
         g.fillRect(selectedBar.x, selectedBar.y, selectedBar.width, selectedBar.height);

         //bounding rectangle
         g.setColor(BACKGROUND_COLOR);
         int boxX = (int) (mouseX - fontMeter.stringWidth(tooltipText) / 2.0) - 5;
         int boxY = mouseY - fontMeter.getHeight() - 8;
         int boxWidth = fontMeter.stringWidth(tooltipText) + 10;
         int boxHeight = fontMeter.getHeight();
         int borderRadius = 2;
         int borderDiameter = borderRadius * 2;
         g.fillRoundRect(boxX, boxY, boxWidth, boxHeight, borderDiameter, borderDiameter); //background

         g.setColor(TEXT_COLOR);
         g.drawRoundRect(boxX, boxY, boxWidth, boxHeight, borderDiameter, borderDiameter); //border

         //tooltip text
         int textX = (int) (mouseX - fontMeter.stringWidth(tooltipText) / 2.0);
         int textY = mouseY - 10;
         g.drawString(tooltipText, textX, textY);
      }
   }

   //Public Setters ********************************************************************************
   @Override
   public void setBackground(Color c)
   {
      super.setBackground(c);
      BACKGROUND_COLOR = c; //other elements depend on the background color
      repaint();
   }

   public void setData(Object[] x, Object[] y)
   {
      xLabels = x;
      dataPoints = y;
      normalizeData();
      repaint();
   }

   public void setBarColor(Color c)
   {
      BAR_COLOR = new Color(c.getRed(), c.getGreen(), c.getBlue(), BAR_OPACITY);
      repaint();
   }

   //Opacity (value is between 0-255)
   public void setBarOpacity(int opacity)
   {
      BAR_OPACITY = opacity;
      Color c = BAR_COLOR;
      BAR_COLOR = new Color(c.getRed(), c.getGreen(), c.getBlue(), BAR_OPACITY);
      repaint();
   }

   public void setTextColor(Color c)
   {
      TEXT_COLOR = c;
      repaint();
   }

   public void setGridColor(Color c)
   {
      GRID_COLOR = c;
      repaint();
   }

   public void setGridLines(int lines)
   {
      GRID_LINES = lines;
      normalizeData();
      repaint();
   }

   public void setGradient(Color start, Color end)
   {
      GRADIENT_START = start;
      GRADIENT_END = end;
      repaint();
   }
}//end class